Using the Enhanced Input System
In game development, handling user input is a crucial part, especially when dealing with complex input sequences or Quick Time Events (QTEs). Dora SSR provides an enhanced input system that allows developers to manage various input events more efficiently and flexibly. This tutorial will guide you on how to set up and use this input system, explaining the new concepts involved in detail.
1. New Concepts Involved
Dora SSR's enhanced input system allows you to create complex input logic, such as multi-stage QTEs and combo keys. By using input contexts, actions, and triggers, you can precisely control how the game responds to player inputs in different states.
1.1 Action
An action is the basic unit in the input system that defines a set of conditions under which a behavior is triggered. For example, pressing the confirm key for confirmation, or pressing the movement key to move a character.
1.2 Input Context
An input context is a collection of actions that allows you to activate or deactivate a set of input actions based on the game scene. For instance, in a game menu scene, you may only need to handle navigation and selection inputs. In an in-game scene, you might need to handle a different set of inputs, such as movement and attack.
1.3 Trigger
A trigger defines the conditions under which an action is activated. It can be a simple key press or a complex input sequence. Dora SSR provides various types of triggers, including:
- KeyDown: Triggered when all specified keys are pressed.
- KeyUp: Triggered when all specified keys are pressed and any one of them is released.
- KeyPressed: Triggered when all specified keys are currently pressed.
- KeyHold: Triggered when a specific key is pressed and held for a specified duration.
- KeyTimed: Triggered when a specific key is pressed within a specified time window.
- KeyDoubleDown: Triggered when a specific key is double-clicked.
- AnyKeyPressed: Triggered when any key is continuously pressed.
- ButtonDown: Triggered when all specified game controller buttons are pressed.
- ButtonUp: Triggered when all specified game controller buttons are pressed and any one of them is released.
- ButtonPressed: Triggered when all specified game controller buttons are currently pressed.
- ButtonHold: Triggered when a specific game controller button is pressed and held for a specified duration.
- ButtonTimed: Triggered when a specific game controller button is pressed within a specified time window.
- ButtonDoubleDown: Triggered when a specific game controller button is double-clicked.
- AnyButtonPressed: Triggered when any game controller button is continuously pressed.
- JoyStick: Triggered when a specific game controller axis is moved.
- JoyStickThreshold: Triggered when the joystick moves beyond a specified threshold.
- JoyStickDirectional: Triggered when the joystick moves in a specific direction within a tolerance angle.
- JoyStickRange: Triggered when the joystick is within a specified range.
- Sequence: Requires triggers to be detected in a specific order.
- Selector: Triggers the action as long as any one trigger is activated.
- Block: Prevents other triggers from being activated.
1.4 Trigger State
When a trigger is activated, it will trigger a corresponding global event in the engine, which contains the current state of the trigger. There are three trigger states:
- Ongoing: The trigger condition is in progress.
- Completed: The trigger condition has been completed.
- Canceled: The trigger condition has been canceled.
1.5 Relationship between Contexts, Actions, Triggers, and Trigger States
An input context contains multiple actions, each action contains a tree-structured organization of triggers, which provide various trigger event sources and the current input state.
1.6 Nesting of Triggers
Here is an example of a tree-nested trigger definition used to describe a trigger for pressing the Ctrl
key and the C
key simultaneously:
The corresponding trigger code definition:
- Lua
- Teal
- TypeScript
- YueScript
Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})
Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})
Trigger.Sequence([
Trigger.KeyPressed(KeyName.LCtrl),
Trigger.KeyDown(KeyName.C)
])
Trigger.Sequence [
Trigger.KeyPressed "LCtrl"
Trigger.KeyDown "C"
]
Here is a trigger definition for pressing and holding the keyboard Enter
key or the game controller A
button for 1 second to trigger a confirmation action:
The corresponding trigger code definition:
- Lua
- Teal
- TypeScript
- YueScript
Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})
Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})
Trigger.Selector([
Trigger.KeyHold(KeyName.Return, 1),
Trigger.ButtonHold(ButtonName.A, 1)
])
Trigger.Selector [
Trigger.KeyHold "Return", 1
Trigger.ButtonHold "a", 1
]
2. Creating the Input System
2.1 Simple Input System Example 1
Here is a simple code example for creating an input system:
- Lua
- Teal
- TypeScript
- YueScript
-- Import modules
local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")
-- Create input manager with one context and one action
local input = InputManager.CreateManager({
testContext = {
["Ctrl+C"] = Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})
}
})
-- Create a node to receive and process input events
local node = Node()
-- Connect global event signals; note the "Input." prefix matches the action's name
node:gslot("Input.Ctrl+C", function(state, progress, value)
if state == "Completed" then
print("Ctrl+C triggered successfully")
-- Remove the current active context, pressing Ctrl+C won't trigger again
input:popContext()
end
end)
-- Activate the testContext to enable its input triggers
input:pushContext("testContext")
-- Import modules
local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")
local type Vec2 = require("Vec2")
-- Create input manager with one context and one action
local input = InputManager.CreateManager({
testContext = {
["Ctrl+C"] = Trigger.Sequence({
Trigger.KeyPressed("LCtrl"),
Trigger.KeyDown("C")
})
}
})
-- Create a node to receive and process input events
local node = Node()
-- Connect global event signals; note the "Input." prefix matches the action's name
node:gslot("Input.Ctrl
+C", function(state: InputManager.TriggerState, progress: number, value: number | boolean | Vec2.Type)
if state == "Completed" then
print("Ctrl+C triggered successfully")
-- Remove the current active context, pressing Ctrl+C won't trigger again
input:popContext()
end
end)
-- Activate the testContext to enable its input triggers
input:pushContext("testContext")
import { Node, KeyName, Vec2 } from "Dora";
import { CreateManager, Trigger, TriggerState } from "InputManager";
// Create input manager with one context and one action
const inputManager = CreateManager({
testContext: {
["Ctrl+C"]: Trigger.Sequence([
Trigger.KeyPressed(KeyName.LCtrl),
Trigger.KeyDown(KeyName.C)
])
}
});
// Create a node to receive and process input events
const node = Node();
// Connect global event signals; note the "Input." prefix matches the action's name
node.gslot("Input.Ctrl+C", (state: TriggerState, progress: number, value: number | boolean | Vec2.Type) => {
if (state === TriggerState.Completed) {
print("Ctrl+C triggered successfully");
// Remove the current active context, pressing Ctrl+C won't trigger again
inputManager.popContext();
}
});
// Activate the testContext to enable its input triggers
inputManager.pushContext("testContext");
_ENV = Dora
import "InputManager" as :CreateManager, :Trigger
-- Create input manager with one context and one action
inputManager = CreateManager
testContext:
["Ctrl+C"]: Trigger.Sequence [
Trigger.KeyPressed "LCtrl"
Trigger.KeyDown "C"
]
-- Create a node to receive and process input events
with Node!
-- Connect global event signals; note the "Input." prefix matches the action's name
\gslot "Input.Ctrl+C", (state, progress, value) ->
if state == "Completed"
print "Ctrl+C triggered successfully"
-- Remove the current active context, pressing Ctrl+C won't trigger again
inputManager\popContext!
-- Activate the testContext to enable its input triggers
inputManager\pushContext "testContext"
In this example, we created an input manager, defined an input context, and one action. The action Ctrl+C
trigger defined the conditions for pressing the Ctrl
key and the C
key. We pushed this context into the input manager for activation. Then we created a scene node to receive and process input events. Finally, we connected the corresponding global event signals, printing a message when the action Ctrl+C
is completed and removing the current active context.
When registering global event signals for handling input events, use the Input.
prefix followed by the action's name, such as Input.Confirm
. Note that in the global event callback function, we can retrieve the trigger's state (state
), progress (progress
), and value (value
). In this example, we only handled the completion state of the action. When using triggers related to time (Hold or Timed), we can obtain the current progress of the trigger through the progress parameter (ranging from 0 to 1). When using triggers that provide varying input values (like joystick axis input), we can retrieve the current input value through the value parameter (value
).
2.2 Simple Input System Example 2
Here is another simple input system example, including a long-press confirmation UI interaction context and a game scene context for character movement:
- Lua
- Teal
- TypeScript
- YueScript
local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")
-- Create input manager with two contexts and their actions
local inputManager = InputManager.CreateManager({
UI = {
Confirm = Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})
},
Game = {
MoveLeft = Trigger.Selector({
Trigger.KeyPressed("Left"),
Trigger.ButtonPressed("dpleft")
}),
MoveRight = Trigger.Selector({
Trigger.KeyPressed("Right"),
Trigger.ButtonPressed("dpright")
})
}
})
-- Create a node to receive and process input events
local node = Node()
-- Connect global event signals to handle the confirm action in the UI context
node:gslot("Input.Confirm", function(state, progress)
if state == "Ongoing" then
print(string.format("Confirming, progress: %d", progress * 100))
elseif state == "Completed" then
print("Confirmation complete")
end
end)
-- Connect global event signals to handle movement actions in the Game context
node:gslot("Input.MoveLeft", function(state)
if state == "Completed" then
print("Moving left")
end
end)
node:gslot("Input.MoveRight", function(state)
if state == "Completed" then
print("Moving right")
end
end)
local InputManager <const> = require("InputManager")
local Trigger <const> = InputManager.Trigger
local Node <const> = require("Node")
-- Create input manager with two contexts and their actions
local inputManager = InputManager.CreateManager({
UI = {
Confirm = Trigger.Selector({
Trigger.KeyHold("Return", 1),
Trigger.ButtonHold("a", 1)
})
},
Game = {
MoveLeft = Trigger.Selector({
Trigger.KeyPressed("Left"),
Trigger.ButtonPressed("dpleft")
}),
MoveRight = Trigger.Selector({
Trigger.KeyPressed("Right"),
Trigger.ButtonPressed("dpright")
})
}
})
-- Create a node to receive and process input events
local node = Node()
-- Connect global event signals to handle the confirm action in the UI context
node:gslot("Input.Confirm", function(state: InputManager.TriggerState, progress: number)
if state == "Ongoing" then
print(string.format("Confirming, progress: %d", progress * 100))
elseif state == "Completed" then
print("Confirmation complete")
end
end)
-- Connect global event signals to handle movement actions in the Game context
node:gslot("Input.MoveLeft", function(state: InputManager.TriggerState)
if state == "Completed" then
print("Moving left")
end
end)
node:gslot("Input.MoveRight", function(state: InputManager.TriggerState)
if state == "Completed" then
print("Moving right")
end
end)
import { Node, KeyName, ButtonName } from "Dora";
import { CreateManager, Trigger, TriggerState } from "InputManager";
// Create input manager with two contexts and their actions
const inputManager = CreateManager({
UI: {
Confirm: Trigger.Selector([
Trigger.KeyHold(KeyName.Return, 1),
Trigger.ButtonHold(ButtonName.A, 1)
])
},
Game: {
MoveLeft: Trigger.Selector([
Trigger.KeyPressed(KeyName.Left),
Trigger.ButtonPressed(ButtonName.Left)
]),
MoveRight: Trigger.Selector([
Trigger.KeyPressed(KeyName.Right),
Trigger.ButtonPressed(ButtonName.Right)
])
}
});
// Create a node to receive and process input events
const node = Node();
// Connect global event signals to handle the confirm action in the UI context
node.gslot("Input.Confirm", (state: TriggerState, progress: number) => {
if (state === TriggerState.Ongoing) {
print(`Confirming, progress: ${progress * 100}`);
} else if (state === TriggerState.Completed) {
print("Confirmation complete");
}
});
// Connect global event signals to handle movement actions in the Game context
node.gslot("Input.MoveLeft", (state: TriggerState) => {
if (state === TriggerState.Completed) {
print("Moving left");
}
});
node.gslot("Input.MoveRight", (state: TriggerState) => {
if (state === TriggerState.Completed) {
print("Moving right");
}
});
_ENV = Dora
import "InputManager" as :CreateManager, :Trigger
-- Create input manager with two contexts and their actions
inputManager = CreateManager
UI:
Confirm: Trigger.Selector [
Trigger.KeyHold "Return", 1
Trigger.ButtonHold "a", 1
]
Game:
MoveLeft: Trigger.Selector [
Trigger.KeyPressed "Left"
Trigger.ButtonPressed "dpleft"
]
MoveRight: Trigger.Selector [
Trigger.KeyPressed "Right"
Trigger.ButtonPressed "dpright"
]
-- Create a node to receive and process input events
with Node!
-- Connect global event signals to handle the confirm action in the UI context
\gslot "Input.Confirm", (state, progress) ->
if state == "Ongoing"
print "Confirming, progress: " + progress * 100
elseif state == "Completed"
print "Confirmation complete"
-- Connect global event signals to handle movement actions in the Game context
\gslot "Input.MoveLeft", (state) ->
if state == "Completed"
print "Moving left"
\gslot "Input.MoveRight", (state) ->
if state == "Completed"
print "Moving right"
In this example, we created an input manager that includes two contexts: UI
and Game
. The UI
context contains a long-press confirmation action Confirm
, while the Game
context contains two movement actions MoveLeft
and MoveRight
. We created a node to receive and process input events. We then connected the global event signals to handle the confirm action in the UI context and the movement actions in the Game context.
When handling the confirm action in the UI context, we can also retrieve the current trigger state and the long-press progress. When handling the movement actions in the Game context, we only need to handle the action completion state.
In actual games, we can dynamically activate or deactivate different input contexts based on the current game state to achieve different input logic. When needing to activate or deactivate a context, simply call the pushContext
or popContext
methods.
- Lua
- Teal
- TypeScript
- YueScript
-- Assuming we are currently in a game operation scene
-- Activate the Game context to start handling character movement
inputManager:pushContext("Game")
-- Assuming we need to open a UI interface for a confirmation operation
-- Activate the UI context, automatically deactivating the Game context
inputManager:pushContext("UI")
-- Assuming the UI interface is now closed
-- Deactivate the UI context, then the remaining Game context on the stack will be reactivated
inputManager:popContext()
-- Assuming you need to activate both Game and UI contexts simultaneously to accept two types of input
inputManager:pushContext({"UI", "Game"})
-- Popping the context from the top of the stack
-- Will deactivate the just activated group of two contexts
inputManager:popContext()
-- Assuming we are currently in a game operation scene
-- Activate the Game context to start handling character movement
inputManager:pushContext("Game")
-- Assuming we need to open a UI interface for a confirmation operation
-- Activate the UI context, automatically deactivating the Game context
inputManager:pushContext("UI")
-- Assuming the UI interface is now closed
-- Deactivate the UI context, then the remaining Game context on the stack will be reactivated
inputManager:popContext()
-- Assuming you need to activate both Game and UI contexts simultaneously to accept two types of input
inputManager:pushContext({"UI", "Game"})
-- Popping the context from the top of the stack
-- Will deactivate the just activated group of two contexts
inputManager:popContext()
// Assuming we are currently in a game operation scene
// Activate the Game context to start handling character movement
inputManager.pushContext("Game");
// Assuming we need to open a UI interface for a confirmation operation
// Activate the UI context, automatically deactivating the Game context
inputManager.pushContext("UI");
// Assuming the UI interface is now closed
// Deactivate the UI context, then the remaining Game context on the stack will be reactivated
inputManager.popContext();
// Assuming you need to activate both Game and UI contexts simultaneously to accept two types of input
inputManager.pushContext(["UI", "Game"]);
// Popping the context from the top of the stack
// Will deactivate the just activated group of two contexts
inputManager.popContext();
-- Assuming we are currently in a game operation scene
-- Activate the Game context to start handling character movement
inputManager\pushContext "Game"
-- Assuming we need to open a UI interface for a confirmation operation
-- Activate the UI context, automatically deactivating the Game context
inputManager\pushContext "UI"
-- Assuming the UI interface is now closed
-- Deactivate the UI context, then the remaining Game context on the stack will be reactivated
inputManager\popContext!
-- Assuming you need to activate both Game and UI contexts simultaneously to accept two types of input
inputManager\pushContext ["UI", "Game"]
-- Popping the context from the top of the stack
-- Will deactivate the just activated group of two contexts
inputManager\popContext!
In this example, we demonstrated how to dynamically activate or deactivate different input contexts to switch between different input logics.
Only the context at the top of the input manager stack will be effective, while contexts not at the top will be automatically deactivated. This mechanism helps you keep track of historical input contexts for reactivation when needed.
3. Implementing Complex Input Logic
In the previous examples, we created a simple input system with one context and one action. Now, we will delve into how to use triggers to implement more complex input logic, such as multi-stage Quick Time Events (QTE).